Map是有多个Key=>Value组成的一种数据结构,Key不可重复。
Go里面创建Map的方式
1 | // 创建一个长度为1的Map |
关于Map的结构
创建Map的是可以通过make,声明Map的容量。
通过源码得到结论:如果Map的容量小于8则不需要指定了,否则在创建Map是最好指定容量,这样可以提高性能。避免不停的扩容。
Golang的Map是通过Hash实现的。
1 | // A header for a Go map. |
count:表示Map中Key=>Value数量
flags:表示Map是否处于写入/扩容状态
B:桶的数量为2^B
noverflow:溢出桶的数量
hash0:随机种子
buckets:bucket数组的指针,数组的大小为2^B
oldbuckets:扩容阶段用于记录旧桶使用的溢出桶的地址,仅仅在扩容时非空
nevacuate:扩容阶段要迁移的下一个旧桶编号
extra:指向mapextar的指针,记录溢出桶相关信息
buckets是一个指向bmap的指针
1 | // A bucket for a Go map. |
在Go中,我们定义的Map数组底层是hmap。
hmap中的buckets指向bmap列表,每个bmap中保存了8个key=>value键值对和指向溢出桶的位置overflow。所以实际数据是存在一个个bucket中。
整体结构

查询
- 当我们查询时,go会将key通过hash成64位[由于当前主流机都是64位操作系统,所以计算结果有64个比特位]。
- 通过低B位确定在哪一个桶中,这里的B就是hmap中的B字段。
- 然后遍历桶中的key,这里的key是tophash,是key经过hash之后的高8位。
- 如果遍历到tophash相同,则再对比key。
- 如果key相同则查询成功。
- 如果桶中没有查询到,则会去关联的溢出桶中重复查找。
bucket桶中的key存的是key经过hash后的高8位即tophash。tophash可以帮助快速定位key的位置。如果高8位都不同,则肯定不匹配了。
新增
- 当我们向Map中put一条数据时,go会将key通过hash成64位。
- 通过hashkey的后B位,确定应该存在哪一个bucket桶中。
- 如果bucket桶中key不满8个,则直接添加到第一个槽位。
- 如果bucket桶中key已经达到8个,则会存入溢出桶的第一个空的槽位中,如果没有溢出桶,则创建溢出桶,添加到溢出桶的第一个空的槽位。
删除
- 查询到对应的位置
- 将该槽位的tophash标记为已删除emptyOne/emptyRest
- 内存不会立刻释放。后续的插入可能会重新占用已删除的槽位。
- 在重排整理后会释放占用的槽位。
扩容/重排
map的扩容有两种,等量扩容、2倍扩容
等量扩容/重排整理(sameSizeGrow)
这种扩容,会将原本松散的key分布重新整理。将桶中的后置位提前。减少溢出桶数量,但不会减少桶的数量,即hmap中的B数。
- 重新分配所有的元素到桶结构中
- 合并桶下的溢出桶
触发条件
1 | // runtime/map.go |
- 元素数量 超过负载因子阈值(6.5*2^B),但溢出桶数量过多(超过 2^(B&15))。
- 删除导致大量空槽位,且 溢出桶 的利用率低。
2倍扩容
原本的桶数量不够使用,会创建一个新的大的桶数组,oldbuckets指向旧桶,每次操作时迁移最多2个旧桶,直到所有的桶里数据都迁移完。期间会将数据重排。
- 初始化新桶,数量翻倍,更新B数。buckets指向新桶,oldbuckets指向旧桶。
- 每次插入/删除/修改操作最多迁移两个旧桶。
- 在扩容没有完成之前,get操作在旧桶未迁移的情况下会在旧桶和新桶查询两次。
- 旧桶迁移完成后,删除oldbucket引用,等待GC回收。
触发条件
1 | // runtime/map.go |
- 元素数量 超过负载因子:count > 6.5 * 2^B,负载因子=6.5。
- 溢出桶 过多:溢出桶数量超过2^(B&15)
线程不安全
在同一时刻多个goroutine对map进行读写是不安全的。写入可能会触发扩容,改变到B值。影响到元素分布的桶位置。
一般通过Mutex锁来处理控制并发读写。
不可对Map数据进行取址操作,因为扩容会导致之前的地址无效。